#!/usr/bin/env python

#  hype32.py
#
#  Copyright 2011 Robert Campbell
#
#  This program is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or
#  (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program.  If not, see <http://www.gnu.org/licenses/>.

# Contributors: Ian Campbell: code review

# Version 0.1 - Latest changes:
# - .

# Testing
# - Tan of 9 differs
# - Cosh of all 8's should max out (all-9) but throws an exception instead

# TODO: Page 10: Stack lifting logic for statistics
# TODO: Ctrl-C to copy number to clipboard
# TODO: '?' kb keypress then calc key causes detailed help to scroll on screen?
# TODO: Do SNDC/AREA and SNDC/X  (f and g of [X<>Y])    
# TODO: Do LINEST/Y and LINEST/X (f and g of [Rv])  
# TODO: What if UI held state to go to after funct called? - Also flag if Last X should be captured first?
# TODO: (Ian) Flash key bottom slope (g-function, label) on mouse over just like key face
# TODO: (Ian) Flash LED's at each key press like actual calc
# TODO: (Ian) Add Commas and decimal point at end (of long number?)
# TODO: (Ian) Nameplate is shorter on Ian's screen than on mine

import sys
# from __future__ import division
import math
import cmath
from Tkinter import *
import locale # from http://stackoverflow.com/questions/1823058
locale.setlocale(locale.LC_ALL, '') # 'en_US') 

# Entry/display Modes
# 
DISPLAY = u'DISPLAY' # Show X - no continued editing of current number allowed.   Eligible to be prev mode (for Clear Prefix)
ENTRY   = u'ENTRY'   # Number entry - edit X as string.                           Eligible to be prev mode (for Clear Prefix) 
ENTERED = u'ENTERED' # Disp # just after Enter. No need to re-enter at new digit. Eligible to be prev mode (for Clear Prefix)
EEX     = u'EEX'     # EEX key hit - digits go into exponent.                     Eligible to be prev mode (for Clear Prefix)  
F       = u'F'       # F key hit - wait for key of specific F func
G       = u'G'       # G key hit - wait for key of specific G func
STO     = u'STO'     # STO key hit - wait for digit or .
STODOT  = u'STODOT'  # STO and . pressed - wait for digit
RCL     = u'RCL'     # RCL key hit - wait for digit or .
RCLDOT  = u'RCLDOT'  # RCL and . hit - wait for digit
FIX     = u'FIX'     # FIX key hit - wait for digit
SCI     = u'SCI'     # SCI key hit - wait for digit
ENG     = u'ENG'     # ENG key hit - wait for digit
MANT    = u'MANT'    # MANT key down: displaying mantissa, wait for MANT key up
ERROR   = u'ERROR'   # "Error..." being displayed - next key hit will clear it

# Error codes - including new ones added for this program
ERROR_0  = u'Error  0'              # Improper Mathematical Operation
ERROR_1  = u'Error  1'              # Storage Register Overflow
ERROR_2  = u'Error  2'              # Improper Register Number
ERROR_3  = u'Error  3'              # Improper Statistical Operation
ERROR_4  = u'Error  4'              # (New) Function not found
ERROR_5  = u'Error  5'              # (New) Argument not found
ERROR_9  = u'Error  9'              # Self-check Failure
ERROR_OK = u'-8,8,8,8,8,8,8,8,8,8,' # Self-check Success 

# Roles for user interface elements
Rf = u'f Function'
Rk = u'Key Face'
Rg = u'g Function'
Ro = u'Other'

# SIZE_AND_PLACE = u'272x395-20-40'
SIZE_AND_PLACE = u'322x431-20-40'

# User interface "styles" (colors, fonts, spacing) - defs for keys and lables
STYLES = {
    u'Display':{u'cs':20,u'fg':u"#%02x%02x%02x" % (255,0,0),u'bg':u"#%02x%02x%02x" % (40, 0,0),u'wd':34,u'bd':0,u'px':0,u'hl':0,u'rl':RAISED,u'an':W,u'fn':(u'Arial',12,u'normal')},
    u'Black'  :{u'cs':4, u'fg':u'white'     ,u'bg':u'black'     ,u'wd':5, u'bd':0,u'px':0         ,u'rl':RAISED,u'an':CENTER,u'fn':(u'Verdana',9,u'normal'),u'hl':1},
    u'Grey'   :{u'cs':5, u'fg':u'black'     ,u'bg':u'gray'      ,u'wd':6, u'bd':0,u'px':0         ,u'rl':RAISED,u'an':CENTER,u'fn':(u'Verdana',9,u'normal'),u'hl':1},
    u'Enter'  :{u'cs':8, u'fg':u'white'     ,u'bg':u'black'     ,u'wd':8, u'bd':0,u'px':23        ,u'rl':RAISED,u'an':CENTER,u'fn':(u'Verdana',9,u'normal'),u'hl':1},
    u'F'      :{u'cs':4, u'fg':u'black'     ,u'bg':u'goldenrod' ,u'wd':5, u'bd':0,u'px':1         ,u'rl':FLAT  ,u'an':CENTER,u'fn':(u'Verdana',9,u'normal'),u'hl':0},
    u'G'      :{u'cs':4, u'fg':u'black'     ,u'bg':u'steel blue',u'wd':5, u'bd':0,u'px':1         ,u'rl':FLAT  ,u'an':N     ,u'fn':(u'Verdana',9,u'normal'),u'hl':0},
    u'fBlack' :{u'cs':4, u'fg':u'goldenrod' ,u'bg':u'black'     ,u'wd':4, u'bd':0,u'px':7, u'py':1,u'rl':FLAT  ,u'an':CENTER,u'fn':(u'Verdana',8,u'normal')},
    u'fGrey'  :{u'cs':5, u'fg':u'goldenrod' ,u'bg':u'black'     ,u'wd':5, u'bd':0,u'px':6, u'py':1,u'rl':FLAT  ,u'an':CENTER,u'fn':(u'Verdana',8,u'normal')},
    u'fEnter' :{u'cs':8, u'fg':u'goldenrod' ,u'bg':u'black'     ,u'wd':6, u'bd':0,u'px':27,u'py':1,u'rl':FLAT  ,u'an':CENTER,u'fn':(u'Verdana',8,u'normal')},
    u'fFunc'  :{u'cs':4, u'fg':u'black'     ,u'bg':u'black'     ,u'wd':4, u'bd':0,u'px':6, u'py':1,u'rl':FLAT  ,u'an':CENTER,u'fn':(u'Verdana',8,u'normal')},
    u'gBlack' :{u'cs':4, u'fg':u'steel blue',u'bg':u'black'     ,u'wd':4, u'bd':1,u'px':5, u'py':0,u'rl':RIDGE ,u'an':CENTER,u'fn':(u'Verdana',8,u'normal')},
    u'gGrey'  :{u'cs':5, u'fg':u'steel blue',u'bg':u'gray'      ,u'wd':5, u'bd':0,u'px':6, u'py':0,u'rl':RAISED,u'an':CENTER,u'fn':(u'Verdana',9,u'normal')},
    u'gEnter' :{u'cs':8, u'fg':u'steel blue',u'bg':u'black'     ,u'wd':7, u'bd':1,u'px':28,u'py':0,u'rl':RIDGE ,u'an':CENTER,u'fn':(u'Verdana',8,u'normal')},
    u'gFunc'  :{u'cs':4, u'fg':u'black'     ,u'bg':u'goldenrod' ,u'wd':4, u'bd':1,u'px':5, u'py':0,u'rl':FLAT  ,u'an':CENTER,u'fn':(u'Verdana',8,u'normal')},
    u'gBlue'  :{u'cs':4, u'fg':u'black'     ,u'bg':u'steel blue',u'wd':4, u'bd':1,u'px':5, u'py':0,u'rl':FLAT  ,u'an':CENTER,u'fn':(u'Verdana',8,u'normal')},
    u'Clear'  :{u'cs':20,u'fg':u'goldenrod' ,u'bg':u'black'     ,u'wd':54,u'bd':0,u'px':0, u'py':0,u'rl':FLAT               ,u'fn':(u'Verdana',6,u'normal')},
    u'Spacer' :{u'cs':1, u'fg':u'black'     ,u'bg':u'black'     ,u'wd':1, u'bd':0,u'px':0, u'py':0,u'rl':FLAT               ,u'fn':(u'Verdana',4,u'normal')},
    u'Name'   :{u'cs':20,u'fg':u'black'     ,u'bg':u'gray'      ,u'wd':39,u'bd':0,u'px':0, u'py':0,u'rl':FLAT               ,u'fn':(u'Verdana',8,u'normal')}
    }

# Symbols - for keys and lables
SQRT  = u'\u221A'; MINUS   = u'\u2501'; DOT   = u'\u2027'; SUP1   = u'\u00B9'; RARW  = u'\u279E'; OVLIN = u'\u0305'; SIGMA = u'\u03A3';
PI    = u'\u220F'; MULCRSS = u'\u2A2F'; START = u'\u256D'; SUP2   = u'\u00B2'; RLARW = u'\u21C4'; OVHAT = u'\u0311'; DELTA = u'\u0394';
PRCNT = u'\u0025'; PLUS    = u'\u002B'; END   = u'\u256E'; SUPX   = u'\u1D61'; UPARW = u'\u2191'; ONEOV = u'\u215F';
DEG   = u'\u00B0'; DIV     = u'\u00F7'; LINE  = u'\u2500'; SUPMIN = u'\u207B'; DNARW = u'\u2193'; YHAT  = u'\u0177';

# Layout of keys (buttons) and lables - format: (func,arg,row,col,role,kb-key,style,text)
#    ________________ Gold Labels __________________________         ____________________ Keys ____________________________        ________________ Blue labels _____________________________
UI =(                                                               (u'led',None       ,0 ,0,Rk,u'#32',u'Display',u' '          ),
    (u'set_mode',FIX     ,1 ,0,Rf,u'',u'fBlack',u'FIX'            ),(u'sqrt',None      ,2 ,0,Rk,u'v' ,u'Black',SQRT+u'x'        ),(u'pow',u'X**2'       ,3 ,0,Rg,u'',u'gBlack',u'X'+SUP2          ),
    (u'set_mode',SCI     ,1 ,1,Rf,u'',u'fBlack',u'SCI'            ),(u'reciprocal',None,2 ,1,Rk,u'i' ,u'Black',ONEOV+u'x'       ),(u'pi',None           ,3 ,1,Rg,u'',u'gBlack',PI                 ),
    (u'set_mode',ENG     ,1 ,2,Rf,u'',u'fBlack',u'ENG'            ),(u'pow',u'Y**X'    ,2 ,2,Rk,u'y' ,u'Black',u'y'+SUPX        ),(u'last_x',None       ,3 ,2,Rg,u'',u'gBlack',u'LST x'           ),
    (u'set_mode',F       ,1 ,3,Rf,u'',u'fFunc' ,u' '              ),(u'set_mode',F     ,2, 3,Rk,u'f' ,u'F'    ,u'f'             ),(u'set_mode',F        ,3 ,3,Rg,u'',u'gFunc' ,u' '               ),
    (u'set_mode',G       ,1 ,4,Rf,u'',u'fFunc' ,u' '              ),(u'set_mode',G     ,2, 4,Rk,u'g' ,u'G'    ,u'g'             ),(u'set_mode',G        ,3 ,4,Rg,u'',u'gBlue' ,u' '               ),
    (u'q_func',u'AREA'   ,4 ,0,Rf,u'',u'fBlack',u'Q'              ),(u'exchange',None  ,5 ,0,Rk,u'x' ,u'Black',u'x '+RLARW+u' y'),(u'q_func',u'X'       ,6 ,0,Rg,u'',u'gBlack',u'Q'+SUPMIN+SUP1   ),
    (u'lin_est',u'Y'     ,4 ,1,Rf,u'',u'fBlack',YHAT              ),(u'roll_down',None ,5 ,1,Rk,u'r' ,u'Black',u'R'+DNARW       ),(u'lin_est',u'X'      ,6 ,1,Rg,u'',u'gBlack',u'x'+OVHAT         ),
    (u'lin_reg',None     ,4 ,2,Rf,u'',u'fBlack',u'L.R.'           ),(u'set_mode',STO   ,5 ,2,Rk,u's' ,u'Black',u'STO'           ),(u'cor_coe',None      ,6 ,2,Rg,u'',u'gBlack',u'r'               ),
    (u'mean',None        ,4 ,3,Rf,u'',u'fBlack',u'x'+OVLIN        ),(u'set_mode',RCL   ,5 ,3,Rk,u'l' ,u'Black',u'RCL'           ),(u'std_dev',None      ,6 ,3,Rg,u'',u'gBlack',u's'               ),
    (u'sigma',u'-'       ,4 ,4,Rf,u'',u'fBlack',SIGMA+MINUS       ),(u'sigma',u'+'     ,5 ,4,Rk,u'a' ,u'Black',SIGMA+PLUS      ),(u'factorial',None     ,6 ,4,Rg,u'',u'gBlack',u'n!'              ),
    (u'CLEARLINE',None   ,7 ,0,Ro,u'',u'Clear',START+LINE+LINE+LINE+LINE+LINE+LINE+LINE+LINE+LINE+LINE+LINE+LINE+LINE+LINE+LINE+LINE+LINE+LINE+LINE+LINE+LINE+LINE+LINE+LINE+LINE+
                                      u' CLEAR '+LINE+LINE+LINE+LINE+LINE+LINE+LINE+LINE+LINE+LINE+LINE+LINE+LINE+LINE+LINE+LINE+LINE+LINE+LINE+LINE+LINE+LINE+LINE+LINE+LINE+LINE+END),
    (u'clear',u'prefix'  ,8 ,0,Rf,u'',u'fEnter',u'PREFIX'         ),(u'enter',None     ,9, 0,Rk,u'#13',u'Enter',u'ENTER'+UPARW  ),(u'set_mode',MANT     ,10,0,Rg,u'',u'gEnter',u'MANT'            ),
    (u'clear',u'all'     ,8 ,2,Rf,u'',u'fBlack',u'ALL'            ),(u'chs',None       ,9 ,2,Rk,u'c' ,u'Black',u'CHS'           ),(u'trigunit',u'rad'   ,10,2,Rg,u'',u'gBlack',u'RAD'             ),
    (u'clear',u'reg'     ,8 ,3,Rf,u'',u'fBlack',u'REG'            ),(u'set_mode',EEX   ,9 ,3,Rk,u'e' ,u'Black',u'EEX'           ),(u'trigunit',u'grd'   ,10,3,Rg,u'',u'gBlack',u'GRD'             ),
    (u'clear',u'stat'    ,8 ,4,Rf,u'',u'fBlack',SIGMA             ),(u'clear',u'x'     ,9 ,4,Rk,u'#8',u'Black',u'CLx'           ),(u'trigunit',u'deg'   ,10,4,Rg,u'',u'gBlack',u'DEG'             ),
    (u'convert',u'in'    ,11,0,Rf,u'',u'fGrey' ,RARW+u' in'       ),(u'arithmetic',u'-',12,0,Rk,u'-' ,u'Grey' ,MINUS            ),(u'convert',u'mm'     ,13,0,Rg,u'',u'gGrey' ,RARW+u' mm'        ),
    (u'trig',u'sin'      ,11,1,Rf,u'',u'fGrey' ,u'SIN'            ),(u'digit',u'7'     ,12,1,Rk,u'7' ,u'Grey' ,u'7'             ),(u'trig',u'asin'      ,13,1,Rg,u'',u'gGrey' ,u'SIN'+SUPMIN+SUP1 ),
    (u'trig',u'cos'      ,11,2,Rf,u'',u'fGrey' ,u'COS'            ),(u'digit',u'8'     ,12,2,Rk,u'8' ,u'Grey' ,u'8'             ),(u'trig',u'acos'      ,13,2,Rg,u'',u'gGrey' ,u'COS'+SUPMIN+SUP1 ),
    (u'trig',u'tan'      ,11,3,Rf,u'',u'fGrey' ,u'TAN'            ),(u'digit',u'9'     ,12,3,Rk,u'9' ,u'Grey' ,u'9'             ),(u'trig',u'atan'      ,13,3,Rg,u'',u'gGrey' ,u'TAN'+SUPMIN+SUP1 ),
    (u'convert',u'F'     ,14,0,Rf,u'',u'fGrey' ,RARW+u' '+DEG+u'F'),(u'arithmetic',u'+',15,0,Rk,u'+' ,u'Grey' ,PLUS             ),(u'convert',u'C'      ,16,0,Rg,u'',u'gGrey' ,RARW+u' '+DEG+u'C' ),
    (u'complex',u'rect'  ,14,1,Rf,u'',u'fGrey' ,RARW+u' R'        ),(u'digit',u'4'     ,15,1,Rk,u'4' ,u'Grey' ,u'4'             ),(u'complex',u'polar'  ,16,1,Rg,u'',u'gGrey' ,RARW+u' P'         ),
    (u'angle',u'radians' ,14,2,Rf,u'',u'fGrey' ,RARW+u' RAD'      ),(u'digit',u'5'     ,15,2,Rk,u'5' ,u'Grey' ,u'5'             ),(u'angle',u'degrees'  ,16,2,Rg,u'',u'gGrey' ,RARW+u' DEG'       ),
    (u'hours',u'hms'     ,14,3,Rf,u'',u'fGrey' ,RARW+u' H.MS'     ),(u'digit',u'6'     ,15,3,Rk,u'6' ,u'Grey' ,u'6'             ),(u'hours',u'h'        ,16,3,Rg,u'',u'gGrey' ,RARW+u' H'         ),
    (u'convert',u'lbm'   ,17,0,Rf,u'',u'fGrey' ,RARW+u' lbm'      ),(u'arithmetic',u'*',18,0,Rk,u'*' ,u'Grey' ,MULCRSS          ),(u'convert',u'kg'     ,19,0,Rg,u'',u'gGrey' ,RARW+u' kg'        ),
    (u'hyperb',u'sinh'   ,17,1,Rf,u'',u'fGrey' ,u'SINH'           ),(u'digit',u'1'     ,18,1,Rk,u'1' ,u'Grey' ,u'1'             ),(u'hyperb',u'asinh'   ,19,1,Rg,u'',u'gGrey' ,u'SINH'+SUPMIN+SUP1),
    (u'hyperb',u'cosh'   ,17,2,Rf,u'',u'fGrey' ,u'COSH'           ),(u'digit',u'2'     ,18,2,Rk,u'2' ,u'Grey' ,u'2'             ),(u'hyperb',u'acosh'   ,19,2,Rg,u'',u'gGrey' ,u'COSH'+SUPMIN+SUP1),
    (u'hyperb',u'tanh'   ,17,3,Rf,u'',u'fGrey' ,u'TANH'           ),(u'digit',u'3'     ,18,3,Rk,u'3' ,u'Grey' ,u'3'             ),(u'hyperb',u'atanh'   ,19,3,Rg,u'',u'gGrey' ,u'TANH'+SUPMIN+SUP1),
    (u'convert',u'gal'   ,20,0,Rf,u'',u'fGrey' ,RARW+u' gal'      ),(u'arithmetic',u'/',21,0,Rk,u'/' ,u'Grey' ,DIV              ),(u'convert',u'ltr'    ,22,0,Rg,u'',u'gGrey' ,RARW+u' ltr'       ),
    (u'log',u'e'         ,20,1,Rf,u'',u'fGrey' ,u'LN'             ),(u'digit',u'0'     ,21,1,Rk,u'0' ,u'Grey' ,u'0'             ),(u'pow',u'e**X'       ,22,1,Rg,u'',u'gGrey' ,u'e'+SUPX          ),
    (u'log',u'10'        ,20,2,Rf,u'',u'fGrey' ,u'LOG'            ),(u'digit',u'.'     ,21,2,Rk,u'.' ,u'Grey' ,DOT              ),(u'pow',u'10**X'      ,22,2,Rg,u'',u'gGrey' ,u'10'+SUPX         ),
    (u'percent',u'sigma' ,20,3,Rf,u'',u'fGrey' ,PRCNT+u' '+SIGMA  ),(u'percent',None   ,21,3,Rk,u'%' ,u'Grey' ,PRCNT            ),(u'percent',u'delta'  ,22,3,Rg,u'',u'gGrey' ,DELTA+PRCNT        ),
                                                                   (u'SPACER',None     ,25,0,Ro,u'' ,u'Spacer',u' '),                                          
                                      (u'NAMEPLATE',None,26,0,Ro,u'',u'Name'   ,u'   H  E  W  L  E  T  T    -    P  A  C  K  A  R  D    32E   ')
    )

class UserInterface(object):
    u'''Manage calculator's window, buttons, labels and display'''
    def __init__(self):
        self.notation = u'FIX'  # FIX, SCI, ENG
        self.decimals = 4

        # TODO: Should create window so it can't be resized? Modal?
        # Frame to contain UI elements
        root = Tk()
        root.title(u'Hype32')
        root.resizable(FALSE,FALSE)
        root.geometry(SIZE_AND_PLACE) 
        self.frame = Frame(root, bg=u'Black')
        self.frame.pack(expand=YES, fill=BOTH)

        self.tkobjs = [] # Ordered list to: 1)get nearby/related UI objects, 2)retrieve "custom" fields
        self.kbmap  = {} # Maps characters from keyboard key presses to widgets (UI objects)
        self.kbnum  = [] # List of keyboard keys that can only be represented by numbers (no character equivalent)
        
        # Create and place Tk objects based on UI definition
        for (func, arg, row, column, role ,char, style, text) in UI:

            st = STYLES[style]  # Get style details

            # Create label or button?
            if role == Rk:
                tkobj = Button(self.frame,text=text,bg=st[u'bg'],fg=st[u'fg'],font=st[u'fn'],width=st[u'wd'],
                               bd=st[u'bd'],relief=st[u'rl'],padx=st[u'px'],pady=0,highlightthickness=st[u'hl'],anchor=st[u'an'])
            else:
                tkobj =  Label(self.frame,text=text,bg=st[u'bg'],fg=st[u'fg'],font=st[u'fn'],width=st[u'wd'],
                               bd=st[u'bd'],relief=st[u'rl'],padx=st[u'px'],pady=st[u'py'])
            tkobj.grid(row=row,column=column*st[u'cs'],columnspan=st[u'cs'])

            # Create and set object's "custom" (calculator -specific) fields
            tkobj.func  = func
            tkobj.arg   = arg
            tkobj.role  = role
            tkobj.index = len(self.tkobjs)

            # Bind event functions
            tkobj.bind(u'<ButtonPress-1>',  self.key_press)
            tkobj.bind(u'<ButtonRelease-1>',self.key_release)

            self.tkobjs.append(tkobj)   # collect references for later

            if char != u'':              # build map of characters (keyboard keys) to widget indexes
                self.kbmap[char] = tkobj.index    
                if char[0] == u'#':      # keep list of special keyboard keys that can only be expressed as numbers
                    self.kbnum.append(int(char[1:]))
        
        root.bind_all(u'<Key>', self.keyboard)

    def startup(self):
        self.display_number(mem.X)
        self.frame.mainloop()

    def copy_to_clipboard(self):
        u'''Copy number to system clipboard'''
        self.frame.clipboard_clear()
        self.frame.clipboard_append(unicode(mem.X),type=u'STRING')  # TODO: format number?

    def keyboard(self, tkevent):
        u'''Create event based on keyboard key press'''
        use_char = tkevent.char
        if use_char != u'':
            if ord(use_char) in self.kbnum: # Fixup special keys in order to use as map index
                use_char = u'#'+unicode(ord(use_char))
        try:
            self.event(u'KeyRelease',self.tkobjs[self.kbmap[use_char]])  # Lookup calc key associated with kb key
        except KeyError: 
            pass

    def key_press(self, tkevent):
        self.event(u'KeyPress',tkevent.widget)

    def key_release(self, tkevent):
        self.event(u'KeyRelease',tkevent.widget)

    def event(self, action, widget):
        u'''Create event and pass it on to control'''

        # Some UI elements, f (gold) labels for instance, don't translate to key presses
        if widget.role == Rf or widget.role == Ro:
            return

        # Get funcs & args from related UI objects, build possible events
        i = widget.index
        if widget.role == Rk:
            f_payload = (action, self.tkobjs[i-1].func, self.tkobjs[i-1].arg) # prev, next
            k_payload = (action,           widget.func,           widget.arg)
            g_payload = (action, self.tkobjs[i+1].func, self.tkobjs[i+1].arg)
        else:                  # == Rg
            f_payload = (action, self.tkobjs[i-2].func, self.tkobjs[i-2].arg) # prev two
            k_payload = (action, self.tkobjs[i-1].func, self.tkobjs[i-1].arg)
            g_payload = (action,           widget.func,           widget.arg)

        # Process appropriate event based on current mode
        if   ctrl.mode == F: ctrl.process_event(f_payload)
        elif ctrl.mode == G: ctrl.process_event(g_payload)
        else:                ctrl.process_event(k_payload)

    def display_number(self, numobj):

        ostr = ''

        # Number? Format based on notation and decimals
        is_number = True
        try:
            n = float(numobj)
#            Fix this
#            if math.isnan(n):
#                raise ValueError
        except OverflowError:
            n = float(9.999999e99)
        except ValueError:
            ostr = unicode(numobj)
            is_number = False

        if is_number:

            # Automatic Display Switching
            if (abs(n) < float(u'1e-'+unicode(self.decimals)) and abs(n) > 0.0) or    \
                abs(n) > float(u'9999999999'):
                use_notation = u'SCI'
            else:
                use_notation = self.notation

            # Format output based on current notation
            print use_notation
            if use_notation == u'SCI':
                temp     = locale.format(u'%0.'+unicode(self.decimals)+u'e', n, grouping=False) 
                print temp
                temp     = temp.split(u'e')
                base     = temp[0]
                exponent = temp[1]
                if exponent[0] == u'+':
                    exponent = exponent[1:]
                spaces   = u' ' * (9 - len(base))
                ostr     = base+spaces+exponent
            elif use_notation == u'ENG':
                # TODO: log10(0) errors!
                int_log  = int(math.log10(abs(n)))
                exponent = int_log - (int_log % 3)
                base     = n / float(10**exponent)
                base     = locale.format(u'%0.'+unicode(self.decimals)+u'f', base, grouping=True)
                exponent = locale.format(u'%02d', exponent, grouping=True)
                spaces   = u' ' * (9 - len(base))
                ostr     = base+spaces+exponent
            else: # 'FIX'
                ostr     = locale.format(u'%0.'+unicode(self.decimals)+u'f', n, grouping=True) 

        self.display_string(ostr)

    def display_mantissa(self, number):
        u'''Show 10 digit mantissa of X'''
        mant_str = u''
        passed_nonzero =  False  # to remove leading zeros, detect non-zero
        for c in unicode(mem.X):     # skip all non-digits  
            if (c == u'0'and passed_nonzero) or c in u'123456789':
                mant_str += c
            if c in u'123456789':
                passed_nonzero = True
        self.display_string(mant_str[:10]) 

    def display_eex(self):
        u'''Show digits plus exponent'''
        spacer = u' ' * (8 - len(mem.keybuf) + (2-len(mem.expbuf) ) )    
        self.display_string(mem.keybuf+spacer+mem.expbuf)

    # Must be separate from display number to show keybuf as a string (not converted to a number)
    def display_string(self, ostr):
        # Add spaces inbetween characters
        output = u'    '
        for c in ostr:
            if c == u'.' or c == u',':
                output += c
            else:
                output += u'  '+c
        
        self.tkobjs[0].config(text=output)   # Change label of 'display' button

# ________________________________________________________________________________________________________________________
class Memory(object):
    u'''Store and provide calculator's data'''
    def __init__(self):
        self.zero = float(0)
        self.keybuf = u''    # string of digits, '-', or '.' entered by user for number
        self.expbuf = u'00'  # string of digits or '-' entered byf user for exponent
        self.clear_stack()
        self.clear_reg()
        self.clear_stat()

    def clear_stack(self):
        self.X      =  self.zero
        self.Y      =  self.zero
        self.Z      =  self.zero
        self.T      =  self.zero
        self.LastX  =  self.zero

    def clear_reg(self):
        self.reg    = [self.zero,self.zero,self.zero,self.zero,self.zero,self.zero,self.zero,self.zero,self.zero]

    def clear_stat(self):
        self.n      = self.Ex = self.Ex2 = self.Ey = self.Ey2 = self.Exy = self.zero

    def put_keybuf(self, new_keybuf):
        u''' Validate new number and sync with X register'''
        self.keybuf = new_keybuf
        try:
            n = float(self.keybuf)
#            Fix this
#            if math.isnan(n):
#                raise ValueError
#            else:
            mem.X = float(self.keybuf)
        except ValueError:
            mem.X = self.zero   # TODO: Does this hide problems?

    def put_expbuf(self, new_expbuf):
        u'''Validate new exponent and sync with X register'''
        self.expbuf = new_expbuf
        try:
            n = float(self.keybuf) * float(10)**float(self.expbuf) 
#            Fix this
#            if math.isnan(n):
#                raise ValueError
#            else:
            mem.X = n # TODO: change this in put_keybuf() too?
        except ValueError:
            mem.X = self.zero   # TODO: Does this hide problems?

# ________________________________________________________________________________________________________________________
class Control(object):
    u'''React to events and track calculator's mode'''
    def __init__(self):
        self.mode      = DISPLAY   # The display/key-entry "state"
        self.prev_mode = DISPLAY   # save previous state for Clear Prefix
        self.trig_mode = u'deg'     # Trigonomic units
        self.sto_oper  = u'='       # Pending storage register aritmetic operation
        self.error     = None      # Error string

    def run(self):
        ui.startup()

    def process_event(self, payload):
        u'''Determine function from calculator event, call the function, update display'''

        (event_type, func, arg) = payload

        # Most key down events do nothing
        if event_type == u'KeyPress' and arg != MANT: 
            return

        # Dispatch the event
        if self.mode == ERROR:                           # This event only clears error msg
            self.error = None
            self.set_mode(DISPLAY)
        elif self.mode == MANT and event_type == u'KeyRelease': # Leaving mantissa display
            self.set_mode(DISPLAY)
        else:                                            # Call method based on event's func & arg
            try:
                getattr(self,func)(arg)
            except AttributeError:
                self.set_mode(ERROR)
                self.error = ERROR_4
                self.debug(u'!!!!',func+u':'+unicode(arg))

        # Detect error; Override mode that was set by function
        if self.error != None:
            self.set_mode(ERROR)        

        # Need to update display?
        if   self.mode == ERROR:                           ui.display_string(self.error)
        elif self.mode == DISPLAY or self.mode == ENTERED: ui.display_number(mem.X)
        elif self.mode == ENTRY:                           ui.display_string(mem.keybuf)
        elif self.mode == MANT:                            ui.display_mantissa(mem.X)
        elif self.mode == EEX:                             ui.display_eex()

        self.debug(u'POST', func+u':'+unicode(arg)+u'/'+event_type)

    def debug(self,msg1,msg2):
        if True:
            print msg1.ljust(5),
            print msg2.ljust(25),
            print self.mode.ljust(8),
            print (u'('+mem.keybuf+u','+mem.expbuf+u')'),
            print u'X:{0:.2f}'.format(mem.X),
            print u'Y:{0:.2f}'.format(mem.Y),
            print u'Z:{0:.2f}'.format(mem.Z),
            print u'T:{0:.2f}'.format(mem.T),
            print self.sto_oper,
            for i, reg in enumerate(mem.reg): 
                if reg != mem.zero:
                    print u'R{0}:{1:.2f}'.format(i,reg),
            if mem.n > 0: 
                    stats =           u'[{0:.0f}|{1:.2f}|{2:.2f}|{3:.2f}|{4:.2f}|{5:.2f}]' 
                    print stats.format(  mem.n,  mem.Ex, mem.Ex2,mem.Ey, mem.Ey2,mem.Exy),
            print

    def set_mode(self, new_mode):
        u''' React to keys that change calc's entry/display mode'''

        if new_mode == EEX:               # Starting to enter exponent?
            if self.mode == ENTRY:
                if len(mem.keybuf) > 7:
                    return
            elif self.mode in (DISPLAY, ENTERED):
                mem.put_keybuf(u'1')
            mem.put_expbuf(u'00') 

        if new_mode in (STO, RCL, EEX):   # Need to auto-enter first?  TODO: others?
            if self.mode == DISPLAY:            
                self.enter(None)

        if self.mode in (DISPLAY, ENTRY, ENTERED, EEX): # Record to support Clear Prefix
            self.prev_mode = self.mode
        
        self.mode = new_mode

    def digit(self, digit_char):
        u''' React to numeral or decimal point key press'''

        # Not entering a number?
        if self.storage(digit_char) or self.eex(digit_char) or self.display_control(digit_char):
            return

        if self.mode == DISPLAY:            # Need to auto-enter first?
            self.enter(None)
            self.set_mode(ENTERED)

        if self.mode == ENTERED:            # Need to clear display first?
            mem.put_keybuf(u'')

        dlen = len(mem.keybuf)              # Restrict to 10 digits, 1 '-', & 1 '.'
        dlen -= mem.keybuf.count(u'-')
        dlen -= mem.keybuf.count(u'.')

        if dlen < 10:                       # Append new digit (or '.') to end
            mem.put_keybuf(mem.keybuf + digit_char)

        if mem.keybuf.count(u'.') > 1:       # Enforce only one '.'
            mem.put_keybuf(mem.keybuf[:-1])

        if mem.keybuf[0] == u'.':            # Change '.' to '0.'
            mem.put_keybuf(u'0'+mem.keybuf)

        self.set_mode(ENTRY)

    def storage(self, digit_char):
        u'''Return true if memory operation handled'''
        if self.mode in (STO, RCL) and digit_char == u'.':
            if    self.mode == STO: self.set_mode(STODOT)
            elif  self.mode == RCL: self.set_mode(RCLDOT)
            return True
        elif digit_char != u'.':
            digit = int(digit_char)
            if (self.mode in (STO,   RCL   ) and digit in xrange(9)) or  \
               (self.mode in (STODOT,RCLDOT) and digit in xrange(6)):
                if   self.mode == STO:    mem.reg[digit]  = self.operate(mem.reg[digit],  mem.X, self.sto_oper) 
                elif self.mode == STODOT: mem.stat[digit] = self.operate(mem.stat[digit], mem.X, self.sto_oper) 
                elif self.mode == RCL:    mem.X = mem.reg[digit]
                elif self.mode == RCLDOT: mem.X = mem.stat[digit]
                self.sto_oper = u'='
                self.set_mode(DISPLAY) 
                return True

        return False

    def led(self, unused):
        ui.copy_to_clipboard()

    def display_control(self, digit_char):
        u'''Return true if display control operation handled'''
        if digit_char != u'.':
            digit = int(digit_char)
            if self.mode in (FIX, SCI, ENG):
                ui.notation = self.mode
                ui.decimals = digit
                self.set_mode(DISPLAY)          
                return True
        return False
        
    def eex(self, digit_char):
        u'''Return true if exponent digit handled'''
        if digit_char != u'.' and self.mode == EEX:
            temp = mem.expbuf
            temp += digit_char
            if mem.expbuf[0] == u'-':
                temp = u'-'+temp[-2:]
            else:
                temp = temp[-2:]
            mem.put_expbuf(temp)
            return True
        return False

    def clear(self, scope):
        u'''Clear some or all of memory, or mode'''
        if   scope == u'x'     : mem.X = float(0);                                     self.set_mode(DISPLAY)          
        elif scope == u'reg'   : mem.clear_reg();                                      self.set_mode(DISPLAY)
        elif scope == u'stat'  :                  mem.clear_stat();                    self.set_mode(DISPLAY)
        elif scope == u'all'   : mem.clear_reg(); mem.clear_stack(); mem.clear_stat(); self.set_mode(DISPLAY)
        elif scope == u'prefix': self.sto_oper = u'=';                                  self.mode = self.prev_mode  # Don't use set_mode(), clears exp when going back to EEX

    # Stack operations -------------------------------------------------

    def enter(self, arg):
        u'''React to Enter key'''
        # Self Check? TODO: Possibly verify there is a method for each calc event
        if self.mode == STO:
            mem.clear_stack()
            mem.clear_reg()
            mem.clear_stat()
            self.error = ERROR_OK
            self.set_mode(DISPLAY)
        else: # Regular enter - push stack
            mem.T = mem.Z
            mem.Z = mem.Y
            mem.Y = mem.X
            self.set_mode(ENTERED)

    def roll_down(self, arg):
        u'''React to roll-down key'''
        temp  = mem.X
        mem.X = mem.Y
        mem.Y = mem.Z
        mem.Z = mem.T
        mem.T = temp
        self.set_mode(DISPLAY)

    def exchange(self, arg):
        u'''Swap X and Y'''
        temp  = mem.Y
        mem.Y = mem.X
        mem.X = temp
        self.set_mode(DISPLAY)
        
    # One-number math operations -------------------------------------

    def sqrt(self, unused):
        u'''Square root'''
        mem.LastX = mem.X
        try:
            mem.X  = math.sqrt(mem.X)
        except ValueError:
            self.error = ERROR_0
        self.set_mode(DISPLAY)

    def chs(self, unused):
        u'''Change sign of entry, X, or exponent'''
        if self.mode in (DISPLAY, ENTERED):      # In this mode, keybuf may be invalid
            mem.X *= -1                          # TODO: avoid -0.000
        elif self.mode == ENTRY:
            new_keybuf = u'-'+mem.keybuf          # Assume positive going to negative.
            if len(mem.keybuf) > 0:
                if mem.keybuf[0] == u'-':         # Oops, already negative! Go to positive.
                    new_keybuf = mem.keybuf[1:]  
            mem.put_keybuf(new_keybuf)           
            if self.mode == ENTERED:    
                self.set_mode(DISPLAY)    
        elif self.mode == EEX:
            if mem.expbuf[0] == u'-':
                mem.put_expbuf(mem.expbuf[1:])
            else:
                mem.put_expbuf(u'-'+mem.expbuf)

    def last_x(self, unused):
        u'''React to Last X key'''
        mem.T = mem.Z
        mem.Z = mem.Y
        mem.Y = mem.X
        mem.X = mem.LastX
        self.set_mode(DISPLAY)          

    def reciprocal(self, unused):
        u'''React to 1/x key'''
        mem.LastX = mem.X
        try:
            mem.X = float(u'1') / mem.X
        except ZeroDivisionError:
            self.error = ERROR_0
        self.set_mode(DISPLAY)          

    def pi(self, unused):
        u'''Value of pi'''
        if self.mode == DISPLAY:
            self.enter(None)
        mem.LastX = mem.X
        mem.X = math.pi
        self.set_mode(DISPLAY)         

    def factorial(self, unused):
        u'''Factorial'''
        mem.LastX = mem.X
        try:
            mem.X = math.factorial(mem.X)
        except ValueError:
            self.error = ERROR_0
        self.set_mode(DISPLAY)         

    def convert(self, units):
        u'''Metric conversion'''
        mem.LastX = mem.X
        lookup  = {u'in': u'mm', u'lbm':u'kg'         , u'gal': u'ltr'}
        convert = {u'mm': 25.4, u'kg' : 0.4535923700, u'ltr': 3.785411784}

        if units == u'C':       mem.X = (mem.X - 32.0) /  1.8
        elif units == u'F':     mem.X = mem.X * 1.8 + 32.0
        elif units in convert: mem.X = mem.X * convert[units]
        else:                  mem.X = mem.X * (1.0 / convert[lookup[units]])

        self.set_mode(DISPLAY)          

    def hours(self, to_units):
        u'''Convert between decimal hours (h) and hours, minutes, seconds (hms)'''
        mem.LastX = mem.X
        if to_units == u'hms':
            (fraction, hours)   = math.modf(mem.X)
            (fraction, minutes) = math.modf(fraction * 60)
            (          seconds) =           fraction * 60
            mem.X = hours + minutes / 100 + seconds / 10000
        else: # TODO: this (below) doesn't work - fix it
            (fraction, h1) = math.modf(mem.X)
            (fraction, h2) = math.modf(fraction * 100) 
            (fraction, h3) = math.modf(fraction * 100) 
            mem.X = h1 + h2 / 60 + h3 / 3600
        self.set_mode(DISPLAY)          

    def trigunit(self, units):
        u'''Set trigonometric units: degrees, radians, grads'''
        self.trig_mode = units
        self.set_mode(DISPLAY)

    def trig(self, func_name):
        u'''Perform trigonometric functions'''
        try:
            func = getattr(math,func_name)  # Get the actual function
            if  self.trig_mode  == u'deg':   # math module trig funcs use radians!
                new_x = math.radians(mem.X)
                new_x = func(new_x)
                # TODO: why not: new_x = math.degrees(new_x)?
            elif self.trig_mode == u'grd':
                new_x = mem.X * 0.015707963
                new_x = func(new_x)
                new_x = new_x * 63.661977237
            else:               #  'rad'
                new_x = func(mem.X)

            mem.LastX = mem.X
            mem.X     = new_x
        except AttributeError:
            self.error = ERROR_5

        self.set_mode(DISPLAY)
        
    def angle(self, func_name):
        u'''Angular conversion between degrees and radians''' 
        func = getattr(math,func_name)  # Get the actual function
        mem.LastX = mem.X
        mem.X = func(mem.X)
        self.set_mode(DISPLAY)

    def hyperb(self, func_name):
        u'''Perfrom hyperbolic functions'''
        try:
            func = getattr(math,func_name)  # Get the actual function
            mem.LastX = mem.X
            mem.X = func(mem.X)
        except AttributeError:
            self.error = ERROR_5

        self.set_mode(DISPLAY)        

    def log(self, base):
        u'''Natural (base e) or common (base 10) logarithm'''
        mem.LastX = mem.X
        if   base == u'e' : mem.X = math.log(mem.X)
        elif base == u'10': mem.X = math.log10(mem.X)
        else: self.error = ERROR_5
        self.set_mode(DISPLAY)        

    # Two-number math operations ---------------------------------------

    def arithmetic(self, oper):
        u'''Handle key-presses for stack or storage register -based addition, subtraction, multiplication or division'''
        if self.mode in (STO, STODOT):   # Storage register arithmetic in progress?
            self.sto_oper = oper
        else:                            # Stack-based aritmetic
            mem.LastX = mem.X
            mem.X = self.operate(mem.Y,mem.X,oper)
            if not self.error:
                mem.Y = mem.Z
                mem.Z = mem.T
            self.set_mode(DISPLAY)             

    def operate(self, operand1, operand2, operator):
        u'''Return result of arithmetic operation or simple assignment (for storage)'''
        # Note: d = y oper x
        if    operator == u'=': return(           operand2)
        elif  operator == u'+': return(operand1 + operand2)
        elif  operator == u'-': return(operand1 - operand2)
        elif  operator == u'*': return(operand1 * operand2)
        elif  operator == u'/':
            try:               return(operand1 / operand2)
            except: self.error = ERROR_0
        else:       self.error = ERROR_5
        return(mem.zero)
        
    def percent(self, option):
        u'''X percent of Y, delta: percent change from Y to X, sigma: X percent of summ(X)'''
        try:
            if   option == u'delta': temp = (mem.X - mem.Y) / mem.Y * 100
            elif option == u'sigma': temp = mem.X / mem.Ex * 100
            else:                   temp = mem.X / 100 * mem.Y
            mem.LastX = mem.X
            mem.X = temp
            mem.Y = mem.Z
            mem.Z = mem.T
        except ZeroDivisionError:
            self.error = ERROR_0
        self.set_mode(DISPLAY)          

    def complex(self, func_name):
        u'''Convert between polar and rectangular (rect) coordinates'''
        mem.LastX = mem.X
        if   func_name == u'polar': (mem.X,mem.Y) = cmath.polar(complex(mem.X,mem.Y))
        elif func_name == u'rect' : (mem.X,mem.Y) = cmath.rect(         mem.X,mem.Y )
        else                     : self.error = ERROR_5   
        self.set_mode(DISPLAY)                      

    # Power/raise/exponent --------------------------------------------

    def pow(self, option):
        u'''Power funciton with predefined options'''
        mem.LastX = mem.X
        if   option == u'X**2' : mem.X = math.pow(mem.X, 2)
        elif option == u'10**X': mem.X = math.pow(10, mem.X) 
        elif option == u'e**X' : mem.X = math.exp(mem.X)
        elif option == u'Y**X' : 
            if  (mem.Y == mem.zero and mem.X <= mem.zero         ) or \
                (mem.Y <  mem.zero and float(mem.X) != int(mem.X)):
                self.error = ERROR_0
            else:
                mem.LastX = mem.X
                mem.X = math.pow(mem.Y, mem.X )
                mem.Y = mem.Z
                mem.Z = mem.T
        else: self.error = ERROR_5   
        self.set_mode(DISPLAY)        

    # Statistics operations --------------------------------------------
    
    def sigma(self, operator):
        u'''Summation into statistics registers - add or subtract'''
        
        # Just recalling summations (RCL E+)?
        if self.mode == RCL:
            mem.T = mem.Y
            mem.Z = mem.X
            mem.X = mem.Ex
            mem.Y = mem.Ey
            self.set_mode(DISPLAY)
            return
        
        # Adding or subtracting from stat registers?
        if      operator == u'+':   addsub = 1
        elif    operator == u'-':   addsub = -1
        
        # Update statistics registers
        mem.n   += 1             * addsub
        mem.Ex  += mem.X         * addsub
        mem.Ex2 += mem.X**2      * addsub
        mem.Ey  += mem.Y         * addsub
        mem.Ey2 += mem.Y**2      * addsub
        mem.Exy += mem.X * mem.Y * addsub
    
        # Show updated index of summation
        mem.LastX = mem.X
        mem.X = mem.n
        self.set_mode(DISPLAY)
        
    def mean(self, unused):
        u'''Find averages for X and Y accumulation'''
        try:
            mem.LastX = mem.X
            mem.X = mem.Ex / mem.n
            mem.Y = mem.Ey / mem.n
        except ZeroDivisionError:
            self.error = ERROR_0
        self.set_mode(DISPLAY)          
        
    def std_dev(self, unused):
        u'''Find SAMPLE standard deviation for X and Y accumulation'''
        try:
            mem.LastX = mem.X
            mem.X = math.sqrt( ( mem.n * mem.Ex2 - mem.Ex**2 ) / (mem.n * (mem.n - 1) ) )
            mem.Y = math.sqrt( ( mem.n * mem.Ey2 - mem.Ey**2 ) / (mem.n * (mem.n - 1) ) )
        except ZeroDivisionError:
            self.error = ERROR_0
        self.set_mode(DISPLAY)                  

    def lin_reg(self, unused):
        u'''Linear regression using least squares'''
        try:
            mem.LastX = mem.X
            mem.X = (mem.Ey * mem.Ex2 - mem.Ex * mem.Exy ) / ( mem.n * mem.Ex2 - mem.Ex**2 ) # y-intercept
            mem.Y = (mem.n  * mem.Exy - mem.Ex * mem.Ey  ) / ( mem.n * mem.Ex2 - mem.Ex**2 ) # slope
        except ZeroDivisionError:
            self.error = ERROR_3
        self.set_mode(DISPLAY)                  
        
    def cor_coe(self, unused):
        u'''Correlation coefficient'''
        try:
            mem.LastX = mem.X
            mem.X = (mem.Exy - mem.Ex * mem.Ey / mem.n ) / math.sqrt( (mem.Ex2 - mem.Ex**2 / mem.n) * (mem.Ey2 - mem.Ey**2 / mem.n) )
        except ZeroDivisionError:
            self.error = ERROR_3
        self.set_mode(DISPLAY)                  

    def lin_est(self, target):
        u'''Linear estimate'''
        try:
            mem.LastX = mem.X
            B = (mem.Exy - mem.Ex * mem.Ey / mem.n ) / (mem.Ex2 - mem.Ex**2 / mem.n )
            A = mem.Ey / mem.n - B * mem.Ex / mem.n
            if   target == u'X': mem.X = (mem.X - A ) / B
            elif target == u'Y': mem.X = A + B * mem.X
            else              : self.error = ERROR_5 
        except ZeroDivisionError:
            self.error = ERROR_3
        self.set_mode(DISPLAY)                  

    def q_func(self, target):
        u'''Area under standar normal distribution curve to the left of x'''
        if target == u'AREA':
            try:
                mem.LastX = mem.X
                mem.X = 1.0 - (0.5 * (1.0 - self.erf(mem.X / math.sqrt(2.0)))) # http://en.wikipedia.org/wiki/Q-function
            except ZeroDivisionError:
                self.error = ERROR_3
        else:
            try:
                mem.X = math.sqrt(2.0) * (1.0 - self.erf(1.0 - 2.0 * mem.X))  # http://cnx.org/content/m11537/latest/
            except ZeroDivisionError:
                self.error = ERROR_3

        self.set_mode(DISPLAY)                  
            
    
    def erf(self, x):
        u'''Statistical error function aka Gauss error or probability integral'''
        # From http://stackoverflow.com/questions/457408/is-there-an-easily-available-implementation-of-erf-for-python
        # save the sign of x
        sign = 1
        if x < 0: 
            sign = -1
        x = abs(x)

        # constants
        a1 =  0.254829592
        a2 = -0.284496736
        a3 =  1.421413741
        a4 = -1.453152027
        a5 =  1.061405429
        p  =  0.3275911

        # A&S formula 7.1.26
        t = 1.0/(1.0 + p*x)
        y = 1.0 - (((((a5*t + a4)*t) + a3)*t + a2)*t + a1)*t*math.exp(-x*x)
        return sign*y # erf(-x) = -erf(x)

# ________________________________________________________________________________________________________________________
python_version = int(sys.version_info[0])
mem  = Memory()
ctrl = Control()
ui   = UserInterface()
ctrl.run()


